package sketch.ounit;

import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.CatchClause;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.ExpressionStatement;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.Name;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.Statement;
import org.eclipse.jdt.core.dom.StringLiteral;
import org.eclipse.jdt.core.dom.ThrowStatement;
import org.eclipse.jdt.core.dom.TryStatement;
import org.eclipse.jdt.internal.core.dom.NaiveASTFlattener;

import sketch.ast.ASTUtils;
import sketch.instrumenter.SourceCodeInstrumentingVisitor;
import sketch.instrumenter.Tracer;
import sketch.instrumenter.TracerFileReader;
import sketch.util.Checker;

public class ObservationInstantiationVisitor extends NaiveASTFlattener {
	
    public final CompilationUnit unit;
    
    //XXX not stable enough
    public Map<Integer, String> results = null;
    
    public final Map<String, Map<Integer, String>> fileContent;
	
	public ObservationInstantiationVisitor(CompilationUnit unit, Map<String, Map<Integer, String>> fileContent) {
		Checker.checkNull(unit, "The compilation unit could not be null!");
		Checker.checkNull(fileContent, "The results could not be null!");
		this.unit = unit;
		this.fileContent = fileContent;
	}
	
	@Override
	public boolean visit(MethodDeclaration methodDeclaration) {
//		String name = methodDeclaration.getName().getIdentifier();
		String name = SourceCodeInstrumentingVisitor.constructMethodSignature(methodDeclaration, this.unit);
		//check is this method need to instrumented
		if(!fileContent.containsKey(name)) {
			//stop traverse the children of this method
			return false;
		}
		
		Checker.checkTrue(fileContent.containsKey(name), "No method name: " + name + " in the given map + for: " + methodDeclaration);
		results = fileContent.get(name);
		
		return super.visit(methodDeclaration);
	}
	
    @Override
    public boolean visit(TryStatement tryCatch) {
//    	int lineNum = this.unit.getLineNumber(tryCatch.getStartPosition());
    	int startPosition = tryCatch.getStartPosition();
    	if(this.results.containsKey(startPosition)) {
    		String value = this.results.get(startPosition);
    		if(value.equals(Tracer.FAIL.trim())) {
//    			throw new RuntimeException("Have not code this part right now.");
    			AST ast = tryCatch.getAST();
    			List<CatchClause> catches = tryCatch.catchClauses();
    			Checker.checkTrue(catches.size() == 1, "The catches cause size should be 1, but now it is: " + catches.size());
    			List<Statement> statements = catches.get(0).getBody().statements();
    		    MethodInvocation fail = ast.newMethodInvocation();
   			    fail.setExpression(ast.newThisExpression());
   			    fail.setName(ast.newSimpleName("fail"));
   			    Statement insertStatement = ast.newExpressionStatement(fail);
   			    statements.add(insertStatement);
    		} else {
    		  AST ast = tryCatch.getAST();
    		  //do the replacement
    		  //1. clear all in the catch block
    		  //2. rewrite sketch.instrumenter.Tracer.recordFailPosition(1);  ->  fail();
    		  //3. rewrite the exception name Throwable e$$_wrapped -> value
    		  List<CatchClause> catchClauseList = tryCatch.catchClauses();
    		  Checker.checkTrue(catchClauseList.size() == 1, "The size of catch clause list: " + catchClauseList.size()
    				+ " is not 1.");
    		
    		  //step 2
    		  List<Statement> statements = tryCatch.getBody().statements();
    		  //remove the last
    		  Statement lastStatement = statements.get(statements.size() - 1);
    		  //XXX silly compare here
    		  if(lastStatement.toString().trim().startsWith("sketch.instrumenter.Tracer.recordFailPosition")) {
    			statements.remove(statements.size() - 1);
    		  }
    		  //add the fail() statement here
    		  MethodInvocation fail = ast.newMethodInvocation();
			  fail.setExpression(ast.newThisExpression());
			  fail.setName(ast.newSimpleName("fail"));
			  Statement insertStatement = ast.newExpressionStatement(fail);
			  statements.add(insertStatement);
    		
    		  //step 3
    		  CatchClause catchClause = catchClauseList.get(0);
    		  SingleVariableDeclaration exception_declaration = catchClause.getException();
    		  Name typeName = null;
    		  if(value.indexOf(".") == -1) {
    			 //System.out.println(value + "!!!!!!!!");
    			 typeName = ast.newSimpleName(value);
    		  } else {
    			typeName = ASTUtils.createQualifiedName(ast, value);
    		  }
    		  exception_declaration.setType(ast.newSimpleType(typeName));
    		
    		  //step 1
    		  catchClause.getBody().statements().clear();
    		}
    	}
    	return super.visit(tryCatch);
    }
	
	@Override
	public boolean visit(Block block) {
       Checker.checkTrue(unit.getAST() == block.getAST(), "The ast should be equal.");
		
		List<Statement> statements = block.statements();
		Map<Integer, Statement> insertingStatements = new LinkedHashMap<Integer, Statement>();
		int i = 0;
		for(Statement statement : statements) {
			if(statement instanceof ExpressionStatement) {
				ExpressionStatement exprStatement = (ExpressionStatement)statement;
				Expression expression = exprStatement.getExpression();
				if(expression instanceof MethodInvocation
						&& SourceCodeInstrumentingVisitor.isObserverMethod((MethodInvocation)expression)) {
					MethodInvocation methodInvoke = (MethodInvocation)expression;
//					int lineNum = this.unit.getLineNumber(expression.getStartPosition());
					int startPosition = expression.getStartPosition();
					Statement insertStatement = null;
					if(this.results.containsKey(startPosition)) {
						String recordedValue = this.results.get(startPosition);
						//System.out.println("recorded value: " + recordedValue + ", not_null: " + Observer.NOT_NULL);
						if(recordedValue.startsWith(Observer.NOT_NULL)) { //XXX be aware
							MethodInvocation tracerInvoke = methodInvoke.getAST().newMethodInvocation();
							tracerInvoke.setExpression(methodInvoke.getAST().newThisExpression());
							tracerInvoke.setName(methodInvoke.getAST().newSimpleName("assertNotNull"));
							List<Expression> arguments = tracerInvoke.arguments();
							Expression arg = ASTUtils.deepClone((Expression) methodInvoke.arguments().get(0));
							arg.delete();
							arguments.add(arg);
							insertStatement = methodInvoke.getAST().newExpressionStatement(tracerInvoke);
						} else if (recordedValue.equals(Observer.EQUAL)) {
							MethodInvocation tracerInvoke = methodInvoke.getAST().newMethodInvocation();
							tracerInvoke.setExpression(methodInvoke.getAST().newThisExpression());
							tracerInvoke.setName(methodInvoke.getAST().newSimpleName("assertEquals"));
							List<Expression> arguments = tracerInvoke.arguments();
							Expression arg = ASTUtils.deepClone((Expression) methodInvoke.arguments().get(0));
							arg.delete();
							arguments.add(arg);
							Expression arg1 = ASTUtils.deepClone((Expression) methodInvoke.arguments().get(1));
							arg1.delete();
							arguments.add(arg1);
							insertStatement = methodInvoke.getAST().newExpressionStatement(tracerInvoke);
						} else if (recordedValue.equals(Observer.UN_EQUAL)) {
							MethodInvocation tracerInvoke = methodInvoke.getAST().newMethodInvocation();
							tracerInvoke.setExpression(methodInvoke.getAST().newThisExpression());
							tracerInvoke.setName(methodInvoke.getAST().newSimpleName("assertNotSame"));
							List<Expression> arguments = tracerInvoke.arguments();
							Expression arg = ASTUtils.deepClone((Expression) methodInvoke.arguments().get(0));
							arg.delete();
							arguments.add(arg);
							Expression arg1 = ASTUtils.deepClone((Expression) methodInvoke.arguments().get(1));
							arg1.delete();
							arguments.add(arg1);
							insertStatement = methodInvoke.getAST().newExpressionStatement(tracerInvoke);
						} else {
						  //init a statement
						  MethodInvocation tracerInvoke = methodInvoke.getAST().newMethodInvocation();
						  tracerInvoke.setExpression(methodInvoke.getAST().newThisExpression());
						  tracerInvoke.setName(methodInvoke.getAST().newSimpleName("assertEquals"));
						  List<Expression> arguments = tracerInvoke.arguments();
//						  StringLiteral expectedValue = methodInvoke.getAST().newStringLiteral();
//						  expectedValue.setLiteralValue();
						  boolean isString = TracerFileReader.isStringType(recordedValue);
						  recordedValue = TracerFileReader.parseValue(recordedValue);
						  Expression expr;
						  if(isString) {
							  StringLiteral l = methodInvoke.getAST().newStringLiteral();
							  l.setLiteralValue(recordedValue);
							  expr = l;
						  } else {
							  expr = ASTUtils.constantToExpr(recordedValue, methodInvoke.getAST());  
						  }
						  Checker.checkNull(expr, "The expr could not be null.");
						  arguments.add(expr);
						  //the expression
//						  StringLiteral stringLiteral = methodInvoke.getAST().newStringLiteral();
//						  stringLiteral.setLiteralValue(methodInvoke.arguments().get(0).toString());
						  Expression arg = ASTUtils.deepClone((Expression) methodInvoke.arguments().get(0));
						  arg.delete();
						  arguments.add(arg);
						  insertStatement = methodInvoke.getAST().newExpressionStatement(tracerInvoke);
						  //add to the map
						}
					}
					//add to the map
					if(insertStatement != null) {
					    insertingStatements.put(i, insertStatement);
					}
				}
			}
			i++;
		}
		
		if(!insertingStatements.isEmpty()) {
			Integer[] indices = (Integer[])insertingStatements.keySet().toArray(new Integer[0]);
			Arrays.sort(indices);
			int length = indices.length;
			for(int index = length - 1; index > -1; index--) {
				//System.out.println("removing index..." + index);
				Integer lineIndex = indices[index];
				Statement s = insertingStatements.get(lineIndex);
				int removeIndex = lineIndex;
				statements.remove(removeIndex);
				statements.add(lineIndex, s);
			}
		}
		
		return super.visit(block);
	}
	
}
